<?php

namespace pmvc\ioc;

use pmvc\Properties;
use pmvc\log\Log;
use pmvc\util\ClassUtil;

use Exception;
use ReflectionClass;

/**
 * Implementation of the {@link ObjectFactory} that uses
 * {@link ObjectDescriptor}s and {@link PropertyDescriptor}s.
 *
 */
class DescriptorObjectFactory
	implements ObjectFactory {

	private $parent					= NULL;
	private $objects				= Array();
	private $descriptorsById		= Array();
	private $descriptorsByName		= Array();
	private $descriptors			= Array();
	private $initialized			= FALSE;
	private $initializedPostProcess	= FALSE;
	private $preloadedFiles			= FALSE;

	/**
	 * Creates the ObjectFactory with the given
	 * parent.
	 * @param $parent the parent
	 */
	public function __construct(ObjectFactory $parent=NULL) {
		$this->parent = $parent;
	}

	/**
	 * Destructor.
	 */
	public function __destruct() {
		foreach ($this->descriptors as $desc) {
			foreach ($desc->objects as $object) {
				if (ClassUtil::isInstance($object, 'pmvc\ioc\DestroyingObject')) {
					$object->destroy($this);
				}
				if (!empty($desc->destroyMethod)) {
					call_user_func(array($object, $desc->destroyMethod));
				}
			}
		}
	}

	/**
	 * Returns a descriptor by it's id.
	 * @param string $id the id
	 */
	protected function getDescriptorById($id) {
		return (array_key_exists($id, $this->descriptorsById))
			? $this->descriptorsById[$id] : NULL;
	}

	/**
	 * Returns a descriptor by it's name.
	 * @param string $name
	 */
	protected function getDescriptorByName($name) {
		return (array_key_exists($name, $this->descriptorsByName))
			? $this->descriptorsByName[$id] : NULL;
	}

	/**
	 * Retuns descriptors by their type.
	 * @param string $type
	 */
	protected function getDescriptorsByType($type) {
		$ret = Array();
		foreach ($this->descriptors as $desc) {
			if (!$desc->abstract && ClassUtil::classIsA($desc->class, $type)) {
				array_push($ret, $desc);
			}
		}
		return $ret;
	}
	
	/**
	 * Adds an {@link ObjectDescriptor}.
	 * @param ObjectDescriptor $desc
	 */
	public function addObjectDescriptor(ObjectDescriptor $desc) {
		if (!empty($desc->id) 
			&& array_key_exists($desc->id, $this->descriptorsById)) {
			throw new Exception(
				"ObjectDescriptor with id ".$desc->id." exists in this ObjectFactory");
		}
		if (!empty($desc->name)
			&& array_key_exists($desc->name, $this->descriptorsByName)) {
			throw new Exception(
				"ObjectDescriptor with name ".$desc->name." exists in this ObjectFactory");
		}
		if (!empty($desc->id)) { $this->descriptorsById[$desc->id] = $desc; }
		if (!empty($desc->name)) { $this->descriptorsByName[$desc->name] = $desc; }
		array_push($this->descriptors, $desc);
		Log::trace("Added descriptor for: {0}", $desc->class);
	}

	/**
	 * Initializes the ObjectFactory.
	 */
	public function initialize() {
		if ($this->initialized) { return; }
		Log::trace("Initializing ObjectFactory");
		$this->initialized = TRUE;

		$this->preloadedFiles = TRUE;
		Log::trace("Preloading files");
		foreach ($this->descriptors as $desc) {
			if (!empty($desc->preloadFile) && isset($desc->preloadFile)) {
				Log::trace("Preloading {0}", $desc->preloadFile);
				ClassUtil::requireFile($desc->preloadFile, TRUE);
			}
		}

		$this->initializedPostProcess = FALSE;
		Log::trace("Initializing DescriptorPostProcessors");
		foreach ($this->getDescriptorsByType("pmvc\ioc\DescriptorPostProcessor") as $desc) {
			if (!$desc->abstract) {
				$obj = $this->getObjectByDescriptor($desc);
			}
			foreach ($this->descriptors as $desc) {
				$obj->process($desc);
			}
		}
		
		Log::trace("Initializing ObjectPostProcessors");
		foreach ($this->getDescriptorsByType("pmvc\ioc\ObjectPostProcessor") as $desc) {
			$this->getObjectByDescriptor($desc);
		}
		$this->initializedPostProcess = TRUE;
		
		Log::trace("Initializing non lazy singletons");
		foreach ($this->descriptors as $desc) {
			if (!$desc->lazy && !$desc->abstract && $desc->singleton) {
				Log::trace("Initializing non lazy singleton: {0}", $desc->class);
				$this->getObjectByDescriptor($desc);
			}
		}
		
		Log::trace("Initializing of ObjectFactory complete");
	}

	/**
	 * Returns the value of a given property descriptor.
	 * @param unknown_type $prop
	 */
	private function getPropertyDescriptorValue($prop) {

		// special handling for arrays
		if ($prop->type==PropertyDescriptor::TYPE_ARRAY) {
			$ret = Array();
			foreach ($prop->value as $p) {
				if (!empty($p->name)) {
					$ret[$p->name] = $this->getPropertyDescriptorValue($p);
				} else {
					array_push($ret, $this->getPropertyDescriptorValue($p));
				}
			}
			return $ret;
		}

		// all of the other types
		switch($prop->type) {
			case PropertyDescriptor::TYPE_STRING:
				return strval($prop->value);
			case PropertyDescriptor::TYPE_INTEGER:
				return intval($prop->value);
			case PropertyDescriptor::TYPE_FLOAT:
				return floatval($prop->value);
			case PropertyDescriptor::TYPE_BOOLEAN:
				return ($prop->value==1 || trim($prop->value)=="1" || strtolower(trim($prop->value))=="true");
			case PropertyDescriptor::TYPE_JSON:
				return json_decode($prop->value);
			case PropertyDescriptor::TYPE_OBJECT:
				return unserialize($prop->value);
			case PropertyDescriptor::TYPE_REF:
				return $this->getObjectById($prop->value);
			default:
				throw new Exception("Invalid type for property ".$prop->name.": ".$prop->type);
		}
	}

	/**
	 * Returns all of the {@link PropertyDescriptor}s for a
	 * given {@link ObjectDescriptor}.
	 * @param $desc the {@link ObjectDescriptor}
	 * @param $propDeps the {@link PropertyDescriptor}s
	 */
	private function getPropertyDescriptorValues($desc, $propDeps=Array()) {
		if (!empty($desc->parent)) {
			$parentDesc = $this->getDescriptorByid($desc->parent);
			$propDeps = $this->getPropertyDescriptorValues($parentDesc, $propDeps);
		}
		if (!empty($desc->properties)) {
			foreach ($desc->properties as $prop) {
				$propDeps[$prop->name] = $this->getPropertyDescriptorValue($prop);
			}
		}
		return $propDeps;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectsOfType($type, $descIgnore=NULL) {
		$descriptors = $this->getDescriptorsByType($type);
		$ret = Array();
		foreach ($descriptors as $desc) {
			if ($desc!=$descIgnore) {
				array_push($ret, $this->getObjectByDescriptor($desc));
			}
		}
		OrderUtil::sort($ret);
		return $ret;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectById($id) {
		if (!is_string($id)) {
			throw new Exception("balls");
		}
		if (!array_key_exists($id, $this->descriptorsById) && empty($this->parent)) {
			throw new Exception(
				"Unable to find object with id:".$id);
		} else if (!array_key_exists($id, $this->descriptorsById) && !empty($this->parent)) {
			return $this->parent->getObjectById($id);
		}
		return $this->getObjectByDescriptor($this->descriptorsById[$id]);
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectByName($name) {
		if (!array_key_exists($name, $this->descriptorsByName) && empty($this->parent)) {
			throw new Exception(
				"Unable to find object with name: ".$name);
		} else if (!array_key_exists($name, $this->descriptorsByName) && !empty($this->parent)) {
			return $this->parent->getObjectByName($name);
		}
		return $this->getObjectByDescriptor($this->descriptorsByName[$name]);
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectIds() {
		return array_keys($this->descriptorsById);
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectNames() {
		return array_keys($this->descriptorsByName);
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectIdsOfType($type) {
		$ret = Array();
		foreach ($this->descriptorsById as $id => $desc) {
			if (ClassUtil::classIsA($desc->class, $type)) {
				array_push($ret, $id);
			}
		}
		return $ret;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectNamesOfType($type) {
		$ret = Array();
		foreach ($this->descriptorsByName as $name => $desc) {
			if (ClassUtil::classIsA($desc->class, $type)) {
				array_push($ret, $name);
			}
		}
		return $ret;
	}

	/**
	 * {@inheritDoc}
	 */
	function hasObjectWithName($name, $recurse=true) {
		$ret = array_key_exists($name, $this->descriptorsByName);
		if ($ret) { 
			return TRUE; 
		} else if (!empty($this->parent)) {
			return $this->parent->hasObjectWithName($name, $recurse);
		} else {
			return FALSE;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	function hasObjectWithId($name, $recurse=true) {
		$ret = array_key_exists($name, $this->descriptorsById);
		if ($ret) { 
			return TRUE; 
		} else if (!empty($this->parent)) {
			return $this->parent->hasObjectWithId($name, $recurse);
		} else {
			return FALSE;
		}
	}
	
	/**
	 * Creates an object for the given descriptor.
	 * @param ObjectDescriptor $desc
	 */
	private function getObjectByDescriptor($desc) {
		if (!$this->initialized) {
			$this->initialize();
		}

		// return already created singletons
		if (count($desc->objects)>0 && $desc->singleton) {
			return $desc->objects[0];
		}

		// bail if it's abstract
		if ($desc->abstract) {
			throw new Exception("Can't instantiate abstract objects");
		}

		// get the property values
		Log::trace("Instantiating {0}", $desc->class);
		$propDeps = $this->getPropertyDescriptorValues($desc);

		/// get the constructor values
		$ctrDeps = Array();
		if (!empty($desc->constructorArgs)) {
			foreach ($desc->constructorArgs as $prop) {
				$ctrDeps[] = $this->getPropertyDescriptorValue($prop);
			}
		}

		// create the reflection class
		$refClass = NULL;
		if (!empty($desc->factoryClass)) {
			$refClass =  ClassUtil::loadClass($desc->factoryClass);
			if (!ClassUtil::classIsA($desc->factoryClass, 'pmvc\ioc\FactoryObject')) {
				throw new \Exception("Object {0} with factory class of type {1} does not implement FactoryObject",
				$desc->id, $desc->factoryClass);
			}
		} else {
			$refClass = ClassUtil::loadClass($desc->class);
		}

		// instantiate the object
		$object = (count($ctrDeps)>0)
			? $refClass->newInstanceArgs($ctrDeps)
			: $refClass->newInstance();

		// do *aware stuff
		if (ClassUtil::isInstance($object, 'pmvc\ioc\ObjectNameAware')) {
			$object->setObjectName($desc->name);
		}
		if (ClassUtil::isInstance($object, 'pmvc\ioc\ObjectFactoryAware')) {
			$object->setObjectFactory($this);
		}

		// populate it's properties
		foreach ($propDeps as $propName => $propValue) {

			// try the setter
			if ($refClass->hasMethod("set".$propName)) {
				Log::trace("Using method injection for property {1} of {0}", 
					$refClass->getName(), $propName);
				$method = $refClass->getMethod("set".$propName);
				$method->invoke($object, $propValue);

			// try the property
			} else if ($refClass->hasProperty($propName)) {
				Log::trace("Using method injection for property {1} of {0}",
					$refClass->getName(), $propName);
				$prop = $refClass->getProperty($propName);
				$prop->setValue($object, $propValue);

			// zomg
			} else {
				throw new Exception(
					"No property or accessory method for ".$propName
					." found on type: ".$refClass->getName());
			}
		}

		// the replacement object
		$replacementObject = $object;

		// call the factory if needed
		if (!empty($desc->factoryClass)) {
			$replacementObject = $replacementObject->createObject();
		}

		// call post processor
		if ($this->initializedPostProcess) {
			foreach ($this->getObjectsOfType("pmvc\ioc\ObjectPostProcessor", $desc) as $obj) {
				$ret = $obj->processBeforeInitialization($desc->name, $desc->id, $replacementObject);
				if (!empty($ret) && $ret!==FALSE) {
					$replacementObject = $ret;
				}
			}
		} else {
			Log::info("Object '{0}' not eligible for getting processed by ObjectPostProcessors", $desc->id);
		}

		// initialize it if it needs to be
		if (ClassUtil::isInstance($object, 'pmvc\ioc\InitializingObject')) {
			$object->initialize($this);
		}

		// call init method if it needs to be
		if (!empty($desc->initMethod)) {
			call_user_func(array($object, $desc->initMethod));
		}

		// call post processor
		if ($this->initializedPostProcess) {
			foreach ($this->getObjectsOfType("pmvc\ioc\ObjectPostProcessor", $desc) as $obj) {
				$ret = $obj->processAfterInitialization($desc->name, $desc->id, $replacementObject);
				if (!empty($ret) && $ret!==FALSE) {
					$replacementObject = $ret;
				}
			}
		}

		// replace the object
		$object = $replacementObject;

		// return the object
		array_push($this->objects, $object);
		array_push($desc->objects, $object);
		return $object;
	}
	
}

?>